package com.stottlerhenke.simbionic.editor.gui;

import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Vector;
import java.util.regex.Pattern;

import javax.swing.tree.DefaultMutableTreeNode;

import com.stottlerhenke.simbionic.editor.SB_Behavior;
import com.stottlerhenke.simbionic.editor.SB_CancelException;
import com.stottlerhenke.simbionic.editor.SB_EditorDataInterface;
import com.stottlerhenke.simbionic.editor.SB_ErrorInfo;
import com.stottlerhenke.simbionic.editor.SB_TypeManager;
import com.stottlerhenke.simbionic.editor.SB_Variable;
import com.stottlerhenke.simbionic.editor.gui.api.FindMatcher;
import com.stottlerhenke.simbionic.editor.gui.api.I_CompileValidator;

public class SB_Polymorphism extends SB_DrawableComposite {

    private static final long serialVersionUID = 2302585093L + 1002;
    protected SB_Behavior _parent = null;
    public SB_ElementComposite _elements = null;

    public SB_ConnectorComposite _connectors = null;

    public Vector _indices = null;

    public DefaultMutableTreeNode _locals = null;

    protected Rectangle _lastViewRect = null;

    public static final int MAX_STACK_SIZE = 3;

    transient private Vector _undoStack = new Vector();

    transient private Vector _redoStack = new Vector();

    transient private SB_CanvasMomento _momento = null;

    transient private boolean _modified = false;

    public SB_Polymorphism() // constructor for Externalizable object
    {
    }

    public SB_Polymorphism(SB_Behavior parent) {
        _parent = parent;
        _elements = new SB_ElementComposite();
        _connectors = new SB_ConnectorComposite();
        super.add(_elements);
        super.add(_connectors);
        _indices = new Vector();
        _indices.add("Empty");
        _locals = new DefaultMutableTreeNode("Locals");
        _lastViewRect = new Rectangle();
    }

    public void draw(Graphics2D g2) {
        boolean needToResize = _elements.needToResize();
        _elements.draw(g2);
        if (needToResize) _connectors.updatePoints();
        _connectors.draw(g2);
    }

    public void bringHighlightToFront() {
        _elements.bringHighlightToFront();
        _connectors.bringHighlightToFront();
    }

    public boolean add(SB_Drawable drawable) {
        return false;
    }

    public boolean add(SB_Element element) {
        if ((element instanceof SB_Rectangle) && _elements.getInitial() == null) {
            SB_Rectangle rectangle = (SB_Rectangle) element;
            rectangle._initial = true;
        }
        return _elements.add(element);
    }
    
    public String toString(){
        return _elements.toString();
    }

    public boolean add(SB_Connector connector) {
        return _connectors.add(connector);
    }

    protected SB_ElementComposite getElements() {
        return _elements;
    }

    protected SB_ConnectorComposite getConnectors() {
        return _connectors;
    }

    public void write(ObjectOutputStream s, boolean highlightOnly) {
        _elements.write(s, highlightOnly);
        _connectors.write(s, highlightOnly);
    }

    public void read(ObjectInputStream s, boolean highlightOnly) {
        _elements.read(s, highlightOnly);
        _connectors.read(s, highlightOnly);
        if (highlightOnly) _connectors.updatePoints();
    }

    protected void resetIds() {
        _elements.resetIds();
        _connectors.resetIds();
    }

    public String getIndicesLabel() {
        String label = (String) _indices.get(0);
        int size = _indices.size();
        for (int i = 1; i < size; ++i)
            label += " " + (String) _indices.get(i);
        return label;
    }

    public void updateIndices(int type, int index, String name1, String name2) {
        if (_parent.isCore()) return;

        switch (type) {
        case SB_Catalog.kPolyIndexInsert:
            if (index == _indices.size()) {
                _indices.add(name1);
                setModified(true);
            }
            break;
        case SB_Catalog.kPolyIndexDelete:
            if (index < _indices.size()) {
                _indices.remove(index);
                setModified(true);
            }
            break;
        case SB_Catalog.kPolyIndexRename:
            if (index < _indices.size()) {
                if (name1.equals((String) _indices.get(index))) {
                    _indices.set(index, name2);
                    setModified(true);
                }
            }
            break;
        case SB_Catalog.kPolyIndexMoveUp:
            if (index < _indices.size()) {
                _indices.set(index - 1, _indices.set(index, _indices
                        .get(index - 1)));
                setModified(true);
            }
            break;
        case SB_Catalog.kPolyIndexMoveDown:
            if (index < _indices.size() - 1) {
                _indices.set(index + 1, _indices.set(index, _indices
                        .get(index + 1)));
                setModified(true);
            }
            break;
        case SB_Catalog.kPolyIndexSelect:
            if (index >= _indices.size()) _indices.setSize(index + 1);
            _indices.set(index, name1);
            setModified(true);
            break;
        default:
            break;
        }
    }

    public void updateLocalsEditable() {
        int size = _locals.getChildCount();
        for (int i = 0; i < size; ++i) {
            DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) _locals
                    .getChildAt(i);
            ((SB_Variable) childNode.getUserObject()).setEditable(!_parent
                    .isCore());
        }
    }

    protected Vector copyIndices() {
        Vector copy = new Vector();
        int size = _indices.size();
        for (int i = 0; i < size; ++i) {
            String index = (String) _indices.get(i);
            copy.add(new String(index));
        }
        return copy;
    }

    protected void copyLocals(DefaultMutableTreeNode locals) {
        _locals.removeAllChildren();
        int size = locals.getChildCount();
        if (size == 0) return;
        DefaultMutableTreeNode childNode;
        SB_Variable var, copyVar;
        for (int i = 0; i < size; ++i) {
            childNode = (DefaultMutableTreeNode) locals.getChildAt(i);
            var = (SB_Variable) childNode.getUserObject();
            copyVar = new SB_Variable(var.getName(), var.getType());
            copyVar.setEditable(var.isEditable());
            copyVar.setUserData(var.getUserData());
            _locals.add(new DefaultMutableTreeNode(copyVar));
        }
    }

    public boolean isModified() {
        return _modified;
    }

    public void setModified(boolean modified) 
    {
        _modified = modified;
        if(modified) 
        {
            _parent.getEditor().setDirty(true);
            System.out.println("BTN modified: Behavior " + _parent.getName()
                    + ", Poly [" + getIndicesLabel() + "]");
        }
    }

    public void addToUndoStack(SB_CanvasMomento momento) {
        int size = _redoStack.size();
        for (int i = 0; i < size; ++i)
            ((SB_CanvasMomento) _redoStack.get(i))._selection.reset();
        _redoStack.removeAllElements();
        if (_momento != null) {
            _momento._selection.reset();
            _momento = null;
        }
        if (momento == null) momento = new SB_CanvasMomento(this);
        size = _undoStack.size();
        if (size == MAX_STACK_SIZE)
                ((SB_CanvasMomento) _undoStack.remove(0))._selection.reset();
        _undoStack.add(momento);
        if (_parent.getEditor().undoAction != null) {
            _parent.getEditor().undoAction.setEnabled(true);
            _parent.getEditor().redoAction.setEnabled(false);
        }
    }

    public void addToUndoStack() {
        addToUndoStack(null);
    }

    public boolean canUndo() {
        return !_undoStack.isEmpty();
    }

    protected void undo(SB_Canvas canvas) {
        int size = _undoStack.size();
        if (size == 0) return;

        if (_redoStack.size() == MAX_STACK_SIZE)
                ((SB_CanvasMomento) _redoStack.remove(0))._selection.reset();
        if (_momento == null) _momento = new SB_CanvasMomento(this);
        _redoStack.add(_momento);
        if (_parent.getEditor().redoAction != null)
                _parent.getEditor().redoAction.setEnabled(true);

        _momento = (SB_CanvasMomento) _undoStack.remove(size - 1);
        _momento.restore(canvas);
        if (size == 1 && _parent.getEditor().undoAction != null)
                _parent.getEditor().undoAction.setEnabled(false);
    }

    public boolean canRedo() {
        return !_redoStack.isEmpty();
    }

    protected void redo(SB_Canvas canvas) {
        int size = _redoStack.size();
        if (size == 0) return;

        if (_undoStack.size() == MAX_STACK_SIZE)
                ((SB_CanvasMomento) _undoStack.remove(0))._selection.reset();
        if (_momento == null) _momento = new SB_CanvasMomento(this);
        _undoStack.add(_momento);
        if (_parent.getEditor().undoAction != null)
                _parent.getEditor().undoAction.setEnabled(true);

        _momento = (SB_CanvasMomento) _redoStack.remove(size - 1);
        _momento.restore(canvas);
        if (size == 1 && _parent.getEditor().redoAction != null)
                _parent.getEditor().redoAction.setEnabled(false);
    }

    public void compileSIM(BufferedWriter out, SB_ErrorInfo errorInfo, SB_TypeManager typeManager,I_CompileValidator validator) {
        _elements.updateComplex(_parent.getEditor());
        SB_EditorDataInterface.setPoly(this);

        try {
            // hierarchy index
            out.write("\t\t\t");
            int size = _indices.size();
            for (int i = 0; i < size; i++) {
                out.write(SB_ProjectBar.getPString((String) _indices.get(i)));
                if (i < size - 1)
                    out.write(" ");
                else
                    out.newLine();
            }

            // local variables
            size = _locals.getChildCount();
            out.write("\t\t\t\tVariables " + size);
            out.newLine();
            DefaultMutableTreeNode treeNode;
            SB_Variable local;
            for (int i = 0; i < size; ++i) {
                treeNode = (DefaultMutableTreeNode) _locals.getChildAt(i);
                local = (SB_Variable) treeNode.getUserObject();
                out.write("\t\t\t\t\t");
                local.compileSIM(out, typeManager);
                validator.validateLocalVariable(local,this);
                out.newLine();
            }

            // rectangles and conditions
            _elements.compileSIM(out, this, errorInfo, validator);

            // connectors
            _connectors.compileSIM(out, this, errorInfo, validator);
        } catch (IOException exception) {
            System.err.println("i/o exception");
        }
    }

    public void writeExternal(ObjectOutput out) throws IOException {
        super.writeExternal(out);

        out.writeObject(_parent);
        out.writeObject(_elements);
        out.writeObject(_connectors);
        out.writeObject(_indices);
        out.writeObject(_locals);
        out.writeObject(_lastViewRect);
    }

    public void readExternal(ObjectInput in) throws ClassNotFoundException,
            IOException {
        super.readExternal(in);

        _parent = (SB_Behavior) in.readObject();
        _elements = (SB_ElementComposite) in.readObject();
        _connectors = (SB_ConnectorComposite) in.readObject();
        _indices = (Vector) in.readObject();
        _locals = (DefaultMutableTreeNode) in.readObject();
        _lastViewRect = (Rectangle) in.readObject();
    }

    public void exportXML(BufferedWriter out) throws IOException {
        out.write("\t\t\t\t<poly>");
        out.newLine();

        out.write("\t\t\t\t\t<indices>");
        out.newLine();
        int size = _indices.size();
        for (int i = 0; i < size; ++i) {
            String index = (String) _indices.get(i);
        
           index = index.replaceAll("&", "&amp;");
            index = index.replaceAll("<", "&lt;");
            out.write("\t\t\t\t\t\t<index>" + index + "</index>");
            out.newLine();
        }
        out.write("\t\t\t\t\t</indices>");
        out.newLine();

        out.write("\t\t\t\t\t<locals>");
        out.newLine();
        DefaultMutableTreeNode treeNode;
        SB_Variable local;
        size = _locals.getChildCount();
        for (int i = 0; i < size; ++i) {
            treeNode = (DefaultMutableTreeNode) _locals.getChildAt(i);
            local = (SB_Variable) treeNode.getUserObject();
            out.write("\t\t\t\t\t\t<local>");
            out.newLine();
            out.write("\t\t\t\t\t\t\t<name>" + local.getName() + "</name>");
            out.newLine();
            out.write("\t\t\t\t\t\t\t<type>" + local.getType() + "</type>");
            out.newLine();
            out.write("\t\t\t\t\t\t</local>");
            out.newLine();
        }
        out.write("\t\t\t\t\t</locals>");
        out.newLine();

        _elements.exportXML(out);
        _elements.exportConnectors(out); // connected start points
        _connectors.exportXML(out); // unconnected start points

        out.write("\t\t\t\t</poly>");
        out.newLine();
    }

    public int findOccurrences(Pattern pattern, String strReplace)
            throws SB_CancelException {
        int total = 0;
        total += _elements.findOccurrences(pattern, strReplace, this);
        total += _connectors.findOccurrences(pattern, strReplace, this);
        return total;
    }

    /**
     * @return Returns the indices.
     */
    public Vector getIndices() {
        return _indices;
    }

    /**
     * @param indices
     *            The indices to set.
     */
    public void setIndices(Vector indices) {
        _indices = indices;
    }

    /**
     * @return Returns the parent.
     */
    public SB_Behavior getParent() {
        return _parent;
    }
    
    /**
     * Sets the parent behavior.
     */
    public void setParent(SB_Behavior parent)
    {
        _parent = parent;
    }

    /**
     * @return Returns the locals.
     */
    public DefaultMutableTreeNode getLocals() {
        return _locals;
    }

    /**
     * Determines all behaviors that can be invoked directly from this behavior.
     * @return set of behavior names
     */
    public Set getReferencedBehaviors()
    {
        Set behaviorNames = new HashSet();
        
        Iterator elemIt = getElements()._drawables.iterator();
        while (elemIt.hasNext())
        {
            SB_Element element = (SB_Element)elemIt.next();
            if (element instanceof SB_Rectangle)
            {
                String expression = ((SB_Rectangle)element).getExpr();
                int index = expression.indexOf('(');
                if (index != -1)
                {
                    behaviorNames.add( expression.substring(0, index) );
                }
            }
        }
        
        return behaviorNames;
    }
    
    /**
     * Searches this polymorphism for all elements (nodes, conditions, connectors)
     * that meet the given find criteria.
     * @param matcher
     * @return the list of matching elements
     */
    public List findElements(FindMatcher matcher)
    {
        SB_Rectangle initialNode = _elements.getInitial();
        List matches = new ArrayList();
        findElementsAux(initialNode,matcher,matches,new HashSet());
        return matches;
    }

    /**
     * Recursively searches for all elements matching the given condition.
     * @param currrentElement
     * @param matcher
     * @param matchesSoFar
     * @param alreadyVisited
     */
    private void findElementsAux(SB_Drawable currentElement,FindMatcher matcher,List matchesSoFar,HashSet alreadyVisited)
    {
        // check if we've visited this element before (to avoid getting stuck in cycles)
        if (alreadyVisited.contains(currentElement))
            return;
        
        alreadyVisited.add(currentElement);
        
        // check if this element matches the criteria
        if (matcher.matches(currentElement)) {
            matchesSoFar.add(currentElement);
        }
        
        // continue searching from this element
        if (currentElement instanceof SB_Connector) {
            // check the endpoint of this connector
            SB_Element destElement = ((SB_Connector)currentElement)._endElement;
            findElementsAux(destElement,matcher,matchesSoFar,alreadyVisited);
        }
        else {
            // search down each outgoing path from this node until the target is found
            Vector outConnectors = ((SB_Element)currentElement)._connectors;
            for (int i=0; i < outConnectors.size(); ++i) {
                SB_Connector conn = (SB_Connector)outConnectors.get(i);
                findElementsAux(conn._endElement,matcher,matchesSoFar,alreadyVisited);
            }
        }
    }
}